﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace GSdk.Net.Lglcd
{
    using InteropServices;

    /// <summary>
    /// This is the main class of this library and it's, maybe, the only one that will be used (the others are used through this).
    /// Create an instance of this and call Connect to connect to Logitech Gaming Software (LGS from now) (remember to either call Disconnect or Dispose when you have finished!!!).
    /// Then easily access Devices instance property and cast it to DefaultDeviceCollection (if you didn't override DeviceCollectionFactory), or use the utility method
    /// GetDevices that will do the cast for you. Than access any device you want (you must have set support for it in the Applet instance!!!), and trigger an Update to see results.
    /// I suggest to close your application when ConnectionLost is fired, usually because LGS is crashed (this will recover resources on user computer).
    /// </summary>
    public class Applet : IDisposable
    {
        #region Privates & Internals
        private static ulong m_Connections;
        private static IDeviceCollectionFactory m_DeviceCollectionFactory = new DeviceCollectionFactory();

        private string m_Name = string.Empty;
        private bool m_Autostartable;
        private DeviceTypes m_SupportedDevices;
        private DeviceCollection m_Devices;
        private lgLcdOnConfigureCB m_ConfigureHandler;
        private lgLcdOnNotificationCB m_NotificationHandler;
        internal lgLcdConnectContextExW m_ConnectionContext;
        #endregion

        /// <summary>
        /// You can change this with another DeviceCollectionFactory to override all devices behaviours. The Devices property will return an instance of object created by this factory
        /// </summary>
        public static IDeviceCollectionFactory DeviceCollectionFactory
        {
            get { return m_DeviceCollectionFactory; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException();

                m_DeviceCollectionFactory = value;
            }
        }

        #region Publics
        public bool Disposed { get; private set; }

        /// <summary>
        /// This is true if after a Connect we are connected (obviusly is false when ConnectionLost is fired)
        /// </summary>
        public bool Connected { get; private set; }

        /// <summary>
        /// This value can only be read, it changes on some events
        /// </summary>
        public bool Enabled { get; private set; }

        public string Name
        {
            get { return m_Name; }
            set
            {
                if (Connected)
                    throw new InvalidOperationException("Can't change this setting if applet is already connected");

                m_Name = value;
            }
        }

        /// <summary>
        /// This allows the LGS to automatically run this applet when started
        /// </summary>
        public bool Autostartable
        {
            get { return m_Autostartable; }
            set
            {
                if (Connected)
                    throw new InvalidOperationException("Can't change this setting if applet is already connected");

                m_Autostartable = value;
            }
        }

        /// <summary>
        /// This are the device supported by the applet. Care that if you access a device from device collection that is not set here, it will throws an exception
        /// </summary>
        public DeviceTypes SupportedDevices
        {
            get { return m_SupportedDevices; }
            set
            {
                if (Connected)
                    throw new InvalidOperationException("Can't change this setting if applet is already connected");

                m_SupportedDevices = value;
            }
        }

        /// <summary>
        /// Here you can access devices supported by your application and performs graphic updates
        /// </summary>
        public DeviceCollection Devices
        {
            get
            {
                if (!Connected)
                    throw new InvalidOperationException("Applet not connected");

                if (m_Devices == null)
                    m_Devices = DeviceCollectionFactory.CreateDeviceCollection(this);

                return m_Devices;
            }
        }

        /// <summary>
        /// Utility method
        /// </summary>
        /// <typeparam name="T">Subtype of DeviceCollection</typeparam>
        /// <returns>DeviceCollection downcasted to type T</returns>
        public T GetDevices<T>() where T : DeviceCollection
        {
            return Devices as T;
        }

        /// <summary>
        /// Main method of the Applet class, you must call this before any attempt to use a device.
        /// If you call it after is already called and the connection was estabilished, the new call is ignored.
        /// You must call Disconnect when you have finished with the Applet.
        /// Remember that Name, Autostartable and SupportedDevices must be changed before the Connect call, attempts to change those value after
        /// Connected, will throw an exception
        /// </summary>
        public void Connect()
        {
            if (Disposed)
                throw new ObjectDisposedException("Applet: " + Name);
            if (Connected) return;

            m_ConnectionContext.connection = (int)DescriptorErrors.LGLCD_INVALID_CONNECTION;
            m_ConnectionContext.appFriendlyName = Name;
            m_ConnectionContext.isAutostartable = Autostartable;
            m_ConnectionContext.dwAppletCapabilitiesSupported = AppletCapabilities.LGLCD_APPLET_CAP_BASIC;

            if (SupportedDevices.HasFlag(DeviceTypes.BlackAndWhite))
                m_ConnectionContext.dwAppletCapabilitiesSupported |= AppletCapabilities.LGLCD_APPLET_CAP_BW;
            if (SupportedDevices.HasFlag(DeviceTypes.Qvga))
                m_ConnectionContext.dwAppletCapabilitiesSupported |= AppletCapabilities.LGLCD_APPLET_CAP_QVGA;

            InteropMethodsWrapper.Connect(this);

            Connected = true;
        }

        /// <summary>
        /// If you call this you can't perform other operations until Connect is called again
        /// </summary>
        public void Disconnect()
        {
            if (Disposed)
                throw new ObjectDisposedException("Applet: " + Name);
            if (!Connected) return;

            InteropMethodsWrapper.Disconnect(this);
            m_ConnectionContext.connection = (int)DescriptorErrors.LGLCD_INVALID_CONNECTION;

            Connected = false;
        }

        public event EventHandler EnabledChanged;
        public event EventHandler ConnectionLost;
        /// <summary>
        /// This is fired when Configure button is clicked on LGS
        /// </summary>
        public event EventHandler Configure;
        #endregion

        private uint ConfigureHandler([In] int connection, [In] IntPtr pContext)
        {
            OnConfigure();
            return 0U;
        }

        private uint NotificationHandler([In] int connection, [In] IntPtr pContext, [In] Notifications notificationCode, [In] uint notifyParam1, [In] uint notifyParam2, [In] uint notifyParam3, [In] uint notifyParam4)
        {
            DeviceTypes deviceType;
            switch (notificationCode)
            {
                case Notifications.LGLCD_NOTIFICATION_APPLET_ENABLED:
                case Notifications.LGLCD_NOTIFICATION_APPLET_DISABLED:
                    Enabled = notificationCode == Notifications.LGLCD_NOTIFICATION_APPLET_ENABLED;
                    OnEnabledChanged();
                    break;
                case Notifications.LGLCD_NOTIFICATION_CLOSE_CONNECTION:
                    if (Connected)
                    {
                        Connected = false;
                        OnConnectionLost();
                    }
                    break;
                case Notifications.LGLCD_NOTIFICATION_DEVICE_ARRIVAL:
                    deviceType = (DeviceTypes)notifyParam1;
                    try
                    {
                        Devices[deviceType].OnDeviceArrival();
                    }
                    catch (AppletException) { }
                    break;
                case Notifications.LGLCD_NOTIFICATION_DEVICE_REMOVAL:
                    deviceType = (DeviceTypes)notifyParam1;
                    try
                    {
                        Devices[deviceType].OnDeviceRemoval();
                    }
                    catch (AppletException) { }
                    break;
                case Notifications.LGLCD_NOTIFICATION_TERMINATE_APPLET:
                    throw new InvalidOperationException("Invalid notification code (Applet termination code is deprecated!)");
            }
            return 0U;
        }

        #region Event methods
        protected void OnEnabledChanged()
        {
            if (EnabledChanged != null) EnabledChanged(this, EventArgs.Empty);
        }

        protected void OnConnectionLost()
        {
            if (ConnectionLost != null) ConnectionLost(this, EventArgs.Empty);
        }

        protected void OnConfigure()
        {
            if (Configure != null) Configure(this, EventArgs.Empty);
        }
        #endregion

        public Applet()
        {
            if (m_Connections == 0U)
                InteropMethodsWrapper.Initialize();
            ++m_Connections;
            m_ConfigureHandler = new lgLcdOnConfigureCB(ConfigureHandler);
            m_NotificationHandler = new lgLcdOnNotificationCB(NotificationHandler);
            m_ConnectionContext.connection = (int)DescriptorErrors.LGLCD_INVALID_CONNECTION;
            m_ConnectionContext.isPersistent = false;
            m_ConnectionContext.onConfigure = new lgLcdConfigureContext();
            m_ConnectionContext.onConfigure.configCallback = m_ConfigureHandler;
            m_ConnectionContext.onNotify = new lgLcdNotificationContext();
            m_ConnectionContext.onNotify.notificationCallback = m_NotificationHandler;
            Enabled = true;
        }

        #region Dispose handling
        /// <summary>
        /// Calls automatically Disconnect if required
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (Disposed) return;

            if (disposing)
            {
                // Managed resources
            }

            // Unmanaged resources
            try
            {
                Disconnect();
            }
            catch { }

            --m_Connections;
            if (m_Connections == 0U)
                InteropMethodsWrapper.DeInitialize();

            Disposed = true;
        }

        ~Applet()
        {
            Dispose(false);
        }
        #endregion
    }
}
